package com.example.airhockeytextured

import android.content.Context
import android.graphics.BitmapFactory
import android.opengl.GLES30
import android.opengl.GLSurfaceView
import android.opengl.GLUtils
import android.opengl.Matrix
import java.io.BufferedReader
import java.io.InputStreamReader
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.FloatBuffer
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10

class AirHockeyTexturedView(context : Context) : GLSurfaceView(context) {
    init {
        setEGLContextClientVersion(3)
        setRenderer(AirHockeyTexturedRenderer())
    }

    private inner class AirHockeyTexturedRenderer : Renderer {

        private var textureProgram: Int = 0
        private var colorProgram: Int = 0

        private var textureId: Int = 0

        private val projectionMatrix = FloatArray(16)
        private val viewMatrix = FloatArray(16)
        private val modelMatrix = FloatArray(16)
        private val mvpMatrix = FloatArray(16)

        private var tableVaoId: Int = 0
        private var malletVaoId: Int = 0
        private var tableVboId: Int = 0
        private var malletVboId: Int = 0

        private val malletVertices = floatArrayOf(
            // Order of coordinates: X, Y, R, G, B
            0f, -0.4f, 0f, 0f, 1f,
            0f, 0.4f, 1f, 0f, 0f
        )

        private val tableVertices = floatArrayOf(
            // Order of coordinates: X, Y, S, T
            // Triangle Fan
            0f, 0f, 0.5f, 0.5f,
            -0.5f, -0.8f, 0f, 0.9f,
            0.5f, -0.8f, 1f, 0.9f,
            0.5f, 0.8f, 1f, 0.1f,
            -0.5f, 0.8f, 0f, 0.1f,
            -0.5f, -0.8f, 0f, 0.9f
        )

        override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
            GLES30.glClearColor(1.0f, 1.0f, 1.0f, 1.0f)

            colorProgram = createProgram(loadShader(GLES30.GL_VERTEX_SHADER, "simple_vertex_shader.glsl"),
                loadShader(GLES30.GL_FRAGMENT_SHADER, "simple_fragment_shader.glsl"))

            textureProgram = createProgram(loadShader(GLES30.GL_VERTEX_SHADER, "texture_vertex_shader.glsl"),
                loadShader(GLES30.GL_FRAGMENT_SHADER, "texture_fragment_shader.glsl"))

            textureId = loadTexture(context, R.drawable.air_hockey_surface)
            if (textureId == 0) {
                throw RuntimeException("Error loading texture.")
            }
            
            // Create VAOs and VBOs
            val vaoIds = IntArray(2)
            val vboIds = IntArray(2)
            GLES30.glGenVertexArrays(2, vaoIds, 0)
            GLES30.glGenBuffers(2, vboIds, 0)
            tableVaoId = vaoIds[0]
            malletVaoId = vaoIds[1]
            tableVboId = vboIds[0]
            malletVboId = vboIds[1]

            setupTable()
            setupMallet()
        }

        private fun setupTable() {
            GLES30.glUseProgram(textureProgram)
            GLES30.glBindVertexArray(tableVaoId)
            GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, tableVboId)
            GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, tableVertices.size * Float.SIZE_BYTES, 
                ByteBuffer.allocateDirect(tableVertices.size * Float.SIZE_BYTES)
                    .order(ByteOrder.nativeOrder())
                    .asFloatBuffer()
                    .put(tableVertices)
                    .position(0), 
                GLES30.GL_STATIC_DRAW)
            GLES30.glVertexAttribPointer(0, 2, GLES30.GL_FLOAT, false, 4 * Float.SIZE_BYTES, 0)
            GLES30.glEnableVertexAttribArray(0)
            GLES30.glVertexAttribPointer(1, 2, GLES30.GL_FLOAT, false, 4 * Float.SIZE_BYTES, 2 * Float.SIZE_BYTES)
            GLES30.glEnableVertexAttribArray(1)
            GLES30.glBindVertexArray(0)
            GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0)
        }

        private fun setupMallet() {
            GLES30.glUseProgram(colorProgram)
            GLES30.glBindVertexArray(malletVaoId)
            GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, malletVboId)
            GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, malletVertices.size * Float.SIZE_BYTES, 
                ByteBuffer.allocateDirect(malletVertices.size * Float.SIZE_BYTES)
                    .order(ByteOrder.nativeOrder())
                    .asFloatBuffer()
                    .put(malletVertices)
                    .position(0), 
                GLES30.GL_STATIC_DRAW)
            GLES30.glVertexAttribPointer(0, 2, GLES30.GL_FLOAT, false, 5 * Float.SIZE_BYTES, 0)
            GLES30.glEnableVertexAttribArray(0)
            GLES30.glVertexAttribPointer(1, 3, GLES30.GL_FLOAT, false, 5 * Float.SIZE_BYTES, 2 * Float.SIZE_BYTES)
            GLES30.glEnableVertexAttribArray(1)
            GLES30.glBindVertexArray(0)
            GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0)
        }

        override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
            GLES30.glViewport(0, 0, width, height)

            val ratio: Float = width.toFloat() / height.toFloat()
            Matrix.perspectiveM(projectionMatrix, 0, 45.0f, ratio, 1.0f, 10.0f)

            Matrix.setIdentityM(modelMatrix, 0)
            Matrix.translateM(modelMatrix, 0, 0f, 0f, -3f)
            Matrix.rotateM(modelMatrix, 0, -60f, 1f, 0f, 0f)
            Matrix.multiplyMM(mvpMatrix, 0, projectionMatrix, 0, modelMatrix, 0)
        }

        override fun onDrawFrame(gl: GL10?) {
            GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)

            drawTable()
            drawMallet()
        }

        private fun drawTable() {
            GLES30.glUseProgram(textureProgram)
            GLES30.glUniformMatrix4fv(GLES30.glGetUniformLocation(textureProgram, "u_Matrix"), 1, false, mvpMatrix, 0)
            
            // Bind the texture
            GLES30.glActiveTexture(GLES30.GL_TEXTURE0)
            GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId)
            GLES30.glUniform1i(GLES30.glGetUniformLocation(textureProgram, "u_TextureUnit"), 0)

            // Bind table VAO and draw
            GLES30.glBindVertexArray(tableVaoId)
            GLES30.glDrawArrays(GLES30.GL_TRIANGLE_FAN, 0, 6)
            GLES30.glBindVertexArray(0)
        }

        private fun drawMallet() {
            GLES30.glUseProgram(colorProgram)
            GLES30.glUniformMatrix4fv(GLES30.glGetUniformLocation(colorProgram, "u_Matrix"), 1, false, mvpMatrix, 0)

            // Bind mallet VAO and draw
            GLES30.glBindVertexArray(malletVaoId)
            GLES30.glDrawArrays(GLES30.GL_POINTS, 0, 2)
            GLES30.glBindVertexArray(0)
        }
    }

    private fun loadShader(type: Int, filename: String): Int {
        val shaderSource = readShaderFileFromAssets(context, filename)
        val shader = GLES30.glCreateShader(type)
        GLES30.glShaderSource(shader, shaderSource)
        GLES30.glCompileShader(shader)
        val compiled = IntArray(1)
        GLES30.glGetShaderiv(shader, GLES30.GL_COMPILE_STATUS, compiled, 0)
        if (compiled[0] == 0) {
            GLES30.glDeleteShader(shader)
            throw RuntimeException("Could not compile shader: $filename")
        }
        return shader
    }

    private fun loadTexture(context: Context, resourceId: Int): Int {
        val textureObjectIds = IntArray(1)
        GLES30.glGenTextures(1, textureObjectIds, 0)

        if (textureObjectIds[0] == 0) {
            return 0
        }

        val options = BitmapFactory.Options()
        options.inScaled = false // No pre-scaling

        // Load the bitmap from the resource
        val bitmap = BitmapFactory.decodeResource(context.resources, resourceId, options)
        if (bitmap == null) {
            GLES30.glDeleteTextures(1, textureObjectIds, 0)
            return 0
        }
        // Bind to the texture in OpenGL
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureObjectIds[0])

        // Set filtering
        GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR_MIPMAP_LINEAR)
        GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR)

        // Load the bitmap into the bound texture
        GLUtils.texImage2D(GLES30.GL_TEXTURE_2D, 0, bitmap, 0)

        GLES30.glGenerateMipmap(GLES30.GL_TEXTURE_2D)

        // Recycle the bitmap, since its data has been loaded into
        // OpenGL.
        bitmap.recycle()

        // Unbind from the texture.
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0)

        return textureObjectIds[0]
    }

    private fun createProgram(vertexShader: Int, fragmentShader: Int): Int {
        val program = GLES30.glCreateProgram()
        GLES30.glAttachShader(program, vertexShader)
        GLES30.glAttachShader(program, fragmentShader)
        GLES30.glLinkProgram(program)
        val linkStatus = IntArray(1)
        GLES30.glGetProgramiv(program, GLES30.GL_LINK_STATUS, linkStatus, 0)
        if (linkStatus[0] == 0) {
            GLES30.glDeleteProgram(program)
            throw RuntimeException("Could not link program")
        }
        return program
    }

    private fun readShaderFileFromAssets(context: Context, filename: String): String {
        val reader = BufferedReader(InputStreamReader(context.assets.open(filename)))
        val builder = StringBuilder()
        var line: String?
        while (reader.readLine().also { line = it } != null) {
            builder.append(line).append("\n")
        }
        reader.close()
        return builder.toString()
    }

}